﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Forms;
using static DW.Library.Win32API;
using Point = System.Windows.Point;
using DEVMODE = DW.Library.Win32API.DEVMODE;

namespace DW.Library
{
    /// <summary>
    /// Represents a display device or multiple display devices on a single system.
    /// </summary>
    public class Screen
    {
        // References:
        // http://referencesource.microsoft.com/#System.Windows.Forms/ndp/fx/src/winforms/Managed/System/WinForms/Screen.cs
        // http://msdn.microsoft.com/en-us/library/windows/desktop/dd145072.aspx
        // http://msdn.microsoft.com/en-us/library/windows/desktop/dd183314.aspx

        //private readonly IntPtr hmonitor;

        // This identifier is just for us, so that we don't try to call the multimon
        // functions if we just need the primary monitor... this is safer for
        // non-multimon OSes.
        private const int PRIMARY_MONITOR = unchecked((int)0xBAADF00D);

        private const int MONITORINFOF_PRIMARY = 0x00000001;
        private const int MONITOR_DEFAULTTONEAREST = 0x00000002;
        ///// <summary> 主屏幕 </summary>
        //public const string mstrAScreenName = "LEN8B9E";
        ///// <summary> 副屏幕 </summary>
        //public const string mstrBScreenName = "MS_0002";

        private static bool multiMonitorSupport;
        private const int MONITOR_DEFAULTTONULL = 0;

        public enum DpiType
        {
            Effective = 0,
            Angular = 1,
            Raw = 2,
        }

        [DllImport("User32.dll")]
        public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, [In] uint dwFlags);

        [DllImport("Shcore.dll")]
        public static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DpiType dpiType, [Out] out int dpiX, [Out] out int dpiY);

        static Screen()
        {
            multiMonitorSupport = GetSystemMetrics(SM_CMONITORS) != 0;
        }

        private Screen(IntPtr monitor)
            : this(monitor, IntPtr.Zero)
        {
        }
        private Screen(IntPtr monitor, IntPtr hdc, int index)
        {
            Load(monitor, hdc, index);
        }
        private Screen(IntPtr monitor, IntPtr hdc)
        {
            Load(monitor, hdc, 0);

        }
        private void Load(IntPtr monitor, IntPtr hdc, int index)
        {
            if (!multiMonitorSupport || monitor == (IntPtr)PRIMARY_MONITOR)
            {
                this.Bounds = SystemInformation.VirtualScreen;
                this.Primary = true;
                this.DeviceName = "DISPLAY";
            }
            else
            {
                var info = new MONITORINFOEX1();

                GetMonitorInfo(monitor, info);

                this.Primary = ((info.dwFlags & MONITORINFOF_PRIMARY) != 0);

                this.DeviceName = new string(info.szDevice).TrimEnd((char)0);


               Win32API.DEVMODE ndm = NewDevMode();
                EnumDisplaySettings(this.DeviceName, (int)DEVMODE_SETTINGS.ENUM_REGISTRY_SETTINGS, ref ndm);

                this.DEVMODE = ndm;
                this.Bounds = new Rectangle(
                    DEVMODE.dmPosition.x, DEVMODE.dmPosition.y,
                    (int)DEVMODE.dmPelsWidth,
                    (int)DEVMODE.dmPelsHeight);

                string lstrAdpterID = "";
                this.DisplayDeviceID = hmonitor_to_pci_device_id(this.DeviceName, ref lstrAdpterID);
                this.DisplayAdpterID = lstrAdpterID;
                int dpi = GetDPI_Default(DisplayDeviceID);
                if (dpi == -1)
                {
                    var pointScreenDpi = GetPointScreenDpi(new POINTSTRUCT() { x = DEVMODE.dmPosition.x + 1, y = DEVMODE.dmPosition.y + 1 });
                    this.DPI = (uint)(pointScreenDpi * 100);

                }
                this.DPI = (uint)dpi;
            }
            hmonitor = monitor;
        }
        private double GetPointScreenDpi(POINTSTRUCT pOINTSTRUCT)
        {
            double dpi = 0;
            IntPtr monitorHwnd = MonitorFromPoint(pOINTSTRUCT, MONITOR_DEFAULTTONULL);
            GetDpiForMonitor(monitorHwnd, DpiType.Effective, out int dpiX, out int dpiY);
            dpi = Math.Round((double)dpiX / (double)96, 2);
            return dpi;
        }

        private static Win32API.DEVMODE NewDevMode()
        {
            Win32API.DEVMODE dm = new Win32API.DEVMODE();
            dm.dmDeviceName = new String(new char[31]);
            dm.dmFormName = new String(new char[31]);
            dm.dmSize = (ushort)Marshal.SizeOf(dm);
            return dm;
        }

        /// <summary>
        /// Gets an array of all displays on the system.
        /// </summary>
        /// <returns>An enumerable of type Screen, containing all displays on the system.</returns>
        public static IEnumerable<Screen> AllScreens
        {
            get
            {
                if (multiMonitorSupport)
                {
                    var closure = new MonitorEnumCallback();
                    var proc = new MonitorEnumProc(closure.Callback);
                    EnumDisplayMonitors(NullHandleRef, null, proc, IntPtr.Zero);
                    if (closure.Screens.Count > 0)
                    {
                        return closure.Screens.Cast<Screen>();
                    }
                }
                return new[] { new Screen((IntPtr)PRIMARY_MONITOR) };
            }
        }


        /// <summary>
        /// Gets the bounds of the display.
        /// </summary>
        /// <returns>A <see cref="T:System.Windows.Rect" />, representing the bounds of the display.</returns>
        public System.Drawing.Rectangle Bounds
        {
            get; private set;
        }

        public String DisplayDeviceID
        {
            get; private set;
        }

        /// <summary>
        /// 缩放比例
        /// </summary>
        public uint DPI
        {
            get; private set;
        }
        /// <summary>
        /// DEVMODE
        /// </summary>
        public Win32API.DEVMODE DEVMODE { get; set; }

        public String DisplayAdpterID
        {
            get; private set;
        }

        public IntPtr hmonitor
        {
            get; private set;
        }

        /// <summary>
        /// Gets the device name associated with a display.
        /// </summary>
        /// <returns>The device name associated with a display.</returns>
        public string DeviceName
        {
            get; private set;
        }

        /// <summary>
        /// Gets a value indicating whether a particular display is the primary device.
        /// </summary>
        /// <returns>true if this display is primary; otherwise, false.</returns>
        public bool Primary
        {
            get; private set;
        }

        /// <summary>
        /// Gets the primary display.
        /// </summary>
        /// <returns>The primary display.</returns>
        public static Screen PrimaryScreen
        {
            get
            {
                if (multiMonitorSupport)
                {
                    return AllScreens.FirstOrDefault(t => t.Primary);
                }
                return new Screen((IntPtr)PRIMARY_MONITOR);
            }
        }

        /// <summary>
        /// Gets the working area of the display. The working area is the desktop area of the display, excluding taskbars, docked windows, and docked tool bars.
        /// </summary>
        /// <returns>A <see cref="T:System.Windows.Rect" />, representing the working area of the display.</returns>
        public Rectangle WorkingArea
        {
            get
            {
                if (!multiMonitorSupport || hmonitor == (IntPtr)PRIMARY_MONITOR)
                {
                    return SystemInformation.WorkingArea;
                }
                var info = new MONITORINFOEX();
                GetMonitorInfo(hmonitor, info);
                return new Rectangle(
                    info.rcWork.left, info.rcWork.top,
                    info.rcWork.right - info.rcWork.left,
                    info.rcWork.bottom - info.rcWork.top);
            }
        }

        /// <summary>
        /// Retrieves a Screen for the display that contains the largest portion of the specified control.
        /// </summary>
        /// <param name="hwnd">The window handle for which to retrieve the Screen.</param>
        /// <returns>A Screen for the display that contains the largest region of the object. In multiple display environments where no display contains any portion of the specified window, the display closest to the object is returned.</returns>
        public static Screen FromHandle(IntPtr hwnd)
        {
            if (multiMonitorSupport)
            {
                return new Screen(MonitorFromWindow(new HandleRef(null, hwnd), 2));
            }
            return new Screen((IntPtr)PRIMARY_MONITOR);
        }

        /// <summary>
        /// Retrieves a Screen for the display that contains the specified point.
        /// </summary>
        /// <param name="point">A <see cref="T:System.Windows.Point" /> that specifies the location for which to retrieve a Screen.</param>
        /// <returns>A Screen for the display that contains the point. In multiple display environments where no display contains the point, the display closest to the specified point is returned.</returns>
        public static Screen FromPoint(Point point)
        {
            if (multiMonitorSupport)
            {
                var pt = new POINTSTRUCT((int)point.X, (int)point.Y);
                return new Screen(MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST));
            }
            return new Screen((IntPtr)PRIMARY_MONITOR);
        }

        /// <summary>
        /// Gets or sets a value indicating whether the specified object is equal to this Screen.
        /// </summary>
        /// <param name="obj">The object to compare to this Screen.</param>
        /// <returns>true if the specified object is equal to this Screen; otherwise, false.</returns>
        public override bool Equals(object obj)
        {
            var monitor = obj as Screen;
            if (monitor != null)
            {
                if (hmonitor == monitor.hmonitor)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Computes and retrieves a hash code for an object.
        /// </summary>
        /// <returns>A hash code for an object.</returns>
        public override int GetHashCode()
        {
            return (int)hmonitor;
        }

        private class MonitorEnumCallback
        {
            public ArrayList Screens
            {
                get; private set;
            }

            public MonitorEnumCallback()
            {
                this.Screens = new ArrayList();
            }

            public bool Callback(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lparam)
            {
                this.Screens.Add(new Screen(monitor, hdc, Screens.Count));
                return true;
            }
        }

        public static string hmonitor_to_pci_device_id(string nstrName, ref string nstrAdapterID)
        {
            DISPLAY_DEVICE lDisplayDeviceInfo = new DISPLAY_DEVICE();
            lDisplayDeviceInfo.cb = Marshal.SizeOf(lDisplayDeviceInfo);

            uint luiDev = 0;

            while (EnumDisplayDevices(null, luiDev, ref lDisplayDeviceInfo, 0))
            {
                DISPLAY_DEVICE ldd_mon = new DISPLAY_DEVICE();
                ldd_mon.cb = Marshal.SizeOf(lDisplayDeviceInfo);

                uint luiDev_mon = 0;

                while (EnumDisplayDevices(lDisplayDeviceInfo.DeviceName, luiDev_mon, ref ldd_mon, 0))
                {
                    if ((ldd_mon.StateFlags & DisplayDeviceStateFlags.AttachedToDesktop) != 0 && lDisplayDeviceInfo.DeviceName == nstrName)
                    {
                        nstrAdapterID = lDisplayDeviceInfo.DeviceID;
                        return ldd_mon.DeviceID;
                    }
                    luiDev_mon++;
                }

                luiDev++;
            }

            return "";
        }
        static readonly int[] DpiVals = new int[] { 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500 };
        /// <summary>
        /// 默认获取DPI方式
        /// </summary>
        /// <param name="DeviceId">显示器设备ID</param>
        /// <returns></returns>
        public static int GetDPI_Default(string DeviceId)
        {
            try
            {
                int getCountflags = (int)QueryDeviceConfigFlags.QdcOnlyActivePaths;
                var error = GetDisplayConfigBufferSizes(getCountflags, out var pathInfoElementsCount, out var modeInfoElementsCount);

                if (error != 0)
                {
                    return -1;
                }

                //var displayConfigs = new List<DisplayConfigTargetDeviceName>();

                var displayConfigPathInfos = new DisplayConfigPathInfo_DPI[pathInfoElementsCount];
                var displayConfigModeInfos = new DisplayConfigModeInfo[modeInfoElementsCount];

                error = QueryDisplayConfig(getCountflags, ref pathInfoElementsCount, displayConfigPathInfos, ref modeInfoElementsCount, displayConfigModeInfos, IntPtr.Zero);
                if (error != 0)
                {
                    return -1;
                }
                int displayConfigIndex = -1;
                foreach (DisplayConfigPathInfo_DPI displayConfigPathInfo in displayConfigPathInfos)
                {
                    displayConfigIndex++;
                    #region 获取显示器名称
                    DisplayConfigSourceDeviceName source_name = new DisplayConfigSourceDeviceName();
                    source_name.Header.type = (int)DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
                    //source_name.header.size = (uint)Marshal.SizeOf(source_name);
                    source_name.Header.size = (uint)Marshal.SizeOf(
                                typeof(DisplayConfigDeviceInfoNameHeader));

                    //source_name.Header.adapterId = displayConfigPathInfo.sourceInfo.adapterId;
                    //source_name.Header.id = displayConfigPathInfo.sourceInfo.id;
                    error = DisplayConfigGetDeviceInfo(ref source_name.Header);
                    if (error != 0)
                    {
                        continue;
                    }
                    #endregion

                    if (source_name.Header.viewGdiDeviceName != DeviceId)
                    {
                        continue;
                    }
                    #region 获取显示器DPI
                    DisplayConfigSourceDeviceScale dpi_ScaleGet = new DisplayConfigSourceDeviceScale();
                    dpi_ScaleGet.Header.type = (int)DISPLAYCONFIG_DEVICE_INFO_TYPE_CUSTOM.DISPLAYCONFIG_DEVICE_INFO_GET_DPI_SCALE;
                    //source_name.header.size = (uint)Marshal.SizeOf(source_name);
                    dpi_ScaleGet.Header.size = (uint)Marshal.SizeOf(typeof(DisplayConfigDeviceScaleHeader));

                    //dpi_ScaleGet.Header.adapterId = displayConfigPathInfo.sourceInfo.adapterId;
                    //dpi_ScaleGet.Header.id = displayConfigPathInfo.sourceInfo.id;
                    error = DisplayConfigGetDeviceInfo(ref dpi_ScaleGet.Header);
                    if (error != 0)
                    {
                        continue;
                    }
                    /// 当前缩放比例
                    int curScaleRel;
                    /// <summary> 推荐缩放比例</summary>
                    int recommended;
                    /// 最大缩放比例
                    int maximum;

                    if (dpi_ScaleGet.Header.curScaleRel < dpi_ScaleGet.Header.minScaleRel)
                    {
                        dpi_ScaleGet.Header.curScaleRel = dpi_ScaleGet.Header.minScaleRel;
                    }
                    else if (dpi_ScaleGet.Header.curScaleRel > dpi_ScaleGet.Header.maxScaleRel)
                    {
                        dpi_ScaleGet.Header.curScaleRel = dpi_ScaleGet.Header.maxScaleRel;
                    }
                    int minAbs = Math.Abs((int)dpi_ScaleGet.Header.minScaleRel);
                    if (DpiVals.Length >= (minAbs + dpi_ScaleGet.Header.maxScaleRel + 1))
                    {
                        // all ok
                        curScaleRel = DpiVals[minAbs + dpi_ScaleGet.Header.curScaleRel];
                        recommended = DpiVals[minAbs];
                        maximum = DpiVals[minAbs + dpi_ScaleGet.Header.maxScaleRel];
                        return curScaleRel;
                    }
                    #endregion
                }
                return -1;
            }
            catch
            {
                return -1;
            }

        }

    }
}
